"""
Author: yida
Time is: 2022/3/6 09:30
this Code: 代码原文: https://www.cnblogs.com/wj-1314/p/9842719.html
- 代码: 手写数字识别, 源码参考上面的链接, 仅仅包含两个卷积层的手写数字识别 对每个卷积层设置一个权重系数w
- 可直接运行, torchvision会自动下载手写数字识别的数据集 存放在当前文件夹 ./mnist 模型保存为./model.pth
- 未实现测试功能 大家可以自行添加
- 为了便于大家更好的理解可学习参数
- 直接放到代码里面, 边训练边输出, 方便各位理解
"""
import os

import torch
import torch.nn as nn
import torchvision.datasets as normal_datasets
import torchvision.transforms as transforms
from torch.autograd import Variable

os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"


# 两层卷积
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        # 使用序列工具快速构建
        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=5, padding=2),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(2))

        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=5, padding=2),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2))

        self.fc = nn.Linear(7 * 7 * 32, 10)
        self.w = nn.Parameter(torch.ones(2))  # 初始化权重, 对2个卷积分别加一个权重

    def forward(self, x):
        # 归一化权重
        w1 = torch.exp(self.w[0]) / torch.sum(torch.exp(self.w))
        w2 = torch.exp(self.w[1]) / torch.sum(torch.exp(self.w))
        out = self.conv1(x) * w1
        out = self.conv2(out) * w2
        out = out.view(out.size(0), -1)  # reshape
        out = self.fc(out)
        return out


# 将数据处理成Variable, 如果有GPU, 可以转成cuda形式
def get_variable(x):
    x = Variable(x)
    return x.cuda() if torch.cuda.is_available() else x


if __name__ == '__main__':

    num_epochs = 5
    batch_size = 100
    learning_rate = 0.001

    # 从torchvision.datasets中加载一些常用数据集
    train_dataset = normal_datasets.MNIST(
        root='./mnist/',  # 数据集保存路径
        train=True,  # 是否作为训练集
        transform=transforms.ToTensor(),  # 数据如何处理, 可以自己自定义
        download=True)  # 路径下没有的话, 可以下载

    # 见数据加载器和batch
    test_dataset = normal_datasets.MNIST(root='./mnist/',
                                         train=False,
                                         transform=transforms.ToTensor())

    train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                               batch_size=batch_size,
                                               shuffle=True)

    test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                              batch_size=batch_size,
                                              shuffle=False)

    model = CNN()
    if torch.cuda.is_available():
        model = model.cuda()

    # 选择损失函数和优化方法
    loss_func = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

    for epoch in range(num_epochs):
        for i, (images, labels) in enumerate(train_loader):
            images = get_variable(images)
            labels = get_variable(labels)
            outputs = model(images)
            loss = loss_func(outputs, labels)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            if (i + 1) % 100 == 0:
                print('Epoch [%d/%d], Iter [%d/%d] Loss: %.4f'
                      % (epoch + 1, num_epochs, i + 1, len(train_dataset) // batch_size, loss.item()))

                # 动态输出w权重变换
                for name, p in model.named_parameters():
                    if name == 'w':
                        print("特征权重: ", name)
                        w0 = (torch.exp(p[0]) / torch.sum(torch.exp(p))).item()
                        w1 = (torch.exp(p[1]) / torch.sum(torch.exp(p))).item()
                        print("w0={} w1={}".format(w0, w1))
                        print("")

    # Save the Trained Model
    print("训练完成...")
    torch.save(model.state_dict(), './model.pth')
